DLO-JZ Data Augmentation - Jour 4¶

car

Objet du notebook¶

Dans ce TP, on se focalise sur les **problématiques de performance liées à la *Data Augmentation***.

Les opérations de transformation des données d'entrée se font en général sur le CPU. On verra dans ce TP qu'il est possible de les déléguer au GPU si les ressources de calcul offertes par le CPU ne sont pas suffisantes.

Ce TP est divisé en trois parties, correspondant à trois types d'augmentation de données à implémenter :

  • TP 1 : RandAugment sur CPU
  • TP 2 : mixup sur CPU et GPU
  • TP 3 : CutMix sur GPU

Les cellules dans ce notebook ne sont pas prĂ©vues pour ĂȘtre modifiĂ©es, sauf rares exceptions indiquĂ©es dans les commentaires. Les TP se feront en modifiant les codes dlojz_da_X.py.

Les directives de modification seront marquées par l'étiquette TODO dans le notebook suivant.

Les solutions sont présentes dans le répertoire solutions/.

Notebook rédigé par l'équipe assistance IA de l'IDRIS, février 2024


Environnement de calcul¶

Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/2.1.1 :

In [3]:
!module list
Currently Loaded Modulefiles:
 1) cuda/11.8.0              5) openmpi/4.1.5-cuda   9) sparsehash/2.0.3       
 2) nccl/2.18.5-1-cuda       6) intel-mkl/2020.4    10) libjpeg-turbo/2.1.3    
 3) cudnn/8.7.0.84-cuda      7) magma/2.7.1-cuda    11) pytorch-gpu/py3/2.1.1  
 4) gcc/8.5.0(8.3.1:8.4.1)   8) sox/14.4.2          
>

Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.

Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.

TODO : choisir un pseudonyme (maximum 5 caractÚres) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation et la compétition.

In [4]:
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, imagenet_starter, turbo_profiler
MODULE = 'pytorch-gpu/py3/2.1.1'
account = 'for@a100'
name = 'pseudo'   ## Pseudonyme Ă  choisir

Gestion de la queue SLURM¶

Pour afficher vos jobs dans la queue SLURM :

In [ ]:
display_slurm_queue(name)

Remarque: Cette fonction est utilisĂ©e plusieurs fois dans ce notebook. Elle permet d'afficher la queue de maniĂšre dynamique, rafraichie toutes les 5 secondes. Elle ne s'arrĂȘte que lorsque la queue est vide. Si vous dĂ©sirez reprendre la main sur le notebook, il vous suffira d'arrĂȘter manuellement la cellule avec le bouton stop. Cela a bien sĂ»r aucun impact les jobs soumis.

Si vous voulez retirer TOUS vos jobs de la queue SLURM, décommenter et exécuter la cellule suivante :

In [ ]:
#!scancel -u $USER

Si vous voulez retirer UN de vos jobs de la queue SLURM, décommenter, compléter et exécuter la cellule suivante :

In [ ]:
#!scancel <jobid>

Debug¶

Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.

Il est nécessaire dans la cellule suivante d'indiquer le jobid correspondant sous le format donné.

Remarque : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numĂ©ro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi ĂȘtre facilement actualisĂ©e.

In [ ]:
#jobid = ['2088207']

Fichier de sortie :

In [ ]:
%cat {search_log(contains=jobid[0])[0]}

Fichier d'erreur :

In [ ]:
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}

Différence entre deux scripts¶

Pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.

In [26]:
s1 = "dlojz_da_2.py"
s2 = "./solutions/dlojz_da_2.py"
s1 = "mixup.py"
s2="solutions/mixup-solution.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :

compare.html


Garage - Mise à niveau¶

On fixe le batch size et la taille d'image pour ce TP.

In [5]:
bs_optim = 512
image_size = 224

TP_DA_1 : RandAugment¶

Le but de ce TP est d'ajouter la transformation RandAugment (disponible dans torchvision) dans la liste des transformations pour la Data Augmentation et de mesurer son impact sur la performance du code.

Voir la documentation torchvision sur RandAugment.

Vous pouvez exécuter les cellules suivantes pour observer l'effet de la transformation RandAugment :

In [6]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.RandAugment(5, 9),       # Random Augmentation 5: n operations, 9 : magnitude 
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[6]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/sos/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear, antialias=warn)
               RandomHorizontalFlip(p=0.5)
               RandAugment(num_ops=5, magnitude=9, num_magnitude_bins=31, interpolation=InterpolationMode.NEAREST, fill=None)
               ToTensor()
           )
In [7]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=4,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

for i in range(4):
    img = batch[0][i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
X train batch, shape: torch.Size([4, 3, 224, 224]), data type: torch.float32, Memory usage: 2408448 bytes
Y train batch, shape: torch.Size([4]), data type: torch.int64, Memory usage: 32 bytes
CPU times: user 2.7 s, sys: 377 ms, total: 3.08 s
Wall time: 3.14 s

Transformation RandAugment sur CPU¶

TODO : dans le script dlojz_da_1.py :

  • Rajouter la transformation RandAugment dans la liste des transformations des images pour le training avec le paramĂ©trage suivant : Nombre d'opĂ©rations = 5, Magnitude = 9.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [10]:
command = f'dlojz_da_1.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 887754
jobid = ['887754']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [11]:
jobid = ['887754']
In [12]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            887754    gpu_p5     tutu  ssos938  R       2:32      1 jean-zay-iam05

 Done!
In [13]:
controle_technique(jobid)
Train throughput: 2079.58 images/second
GPU throughput: 2430.28 images/second
epoch time: 616.25 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.210675 sec (8.9%/99.2%) +/- 0.074190
loading step time average (IO + CPU to GPU transfer): 0.035528 sec +/- 0.241765
In [14]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 48.483087 s

Commentaires

TP_DA_2 : mixup¶

Le but de ce TP est de :

  • appliquer la transformation mixup et mesurer son impact sur la performance du code ;
  • porter la transformation sur GPU.

La transformation mixup n'est pas disponible dans torchvision, la fonction est disponible dans le script mixup.py. On notera que cette transformation impacte Ă  la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le DataLoader. Donc cette transformation sera faite dans la boucle d'apprentissage aprÚs génération du batch et aprÚs toute autre transformation liée à la Data Augmentation.

Vous pouvez exécuter les cellules suivantes pour observer l'effet de la transformation mixup :

In [15]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[15]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/sos/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear, antialias=warn)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [16]:
from mixup import mixup_data
In [17]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = mixup_data(imgs, targets, num_classes=1000, alpha=2)        ## Transformation mixup

for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 224, 224]), data type: torch.float32, Memory usage: 9633792 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 932, lambda : 0.6358286738395691
target : 760, lambda : 0.5654819011688232
target : 888, lambda : 0.8834490180015564
target : 24, lambda : 0.7696910500526428
CPU times: user 7.45 s, sys: 262 ms, total: 7.71 s
Wall time: 7.93 s

ParamĂštre alpha pour la beta distribution

Dans le script mixup.py, la variable _lambda correspond Ă  la proportion conservĂ©e de la premiĂšre image. Elle est choisie alĂ©atoirement suivant une distribution bĂȘta dĂ©finie sur [0, 1].

Le paramĂštre alpha agit sur la forme de la distribution bĂȘta. alpha = 1 correspond Ă  une distribution uniforme, alpha < 1 favorise un tirage au sort de valeurs proches des bornes 0. ou 1., et alpha > 1 favorise un tirage au sort de valeurs proches du centre 0.5.

In [18]:
for alpha in [0.5, 1., 2.]:
    plt.hist(np.random.beta(alpha, alpha, 1000000), bins=50, density=True, histtype='step')
    plt.title(f'alpha={alpha}')
    plt.show()

Transformation mixup sur CPU¶

TODO : dans le script dlojz_da_2.py :

  • Importer la transformation mixup
from mixup import mixup_data
  • Rajouter la transformation mixup dans la boucle d'apprentissage avant d'envoyer le batch d'images et de labels au GPU, avec le paramĂ©trage : num_classes=1000, alpha=2.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [25]:
command = f'dlojz_da_2.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 887894
jobid = ['887894']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [27]:
jobid = ['887894']
In [28]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            887894    gpu_p5     tutu  ssos938  R       2:38      1 jean-zay-iam33

 Done!
In [29]:
controle_technique(jobid)
Train throughput: 1107.83 images/second
GPU throughput: 2524.46 images/second
epoch time: 1156.80 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.202816 sec (6.1%/110.0%) +/- 0.084701
loading step time average (IO + CPU to GPU transfer): 0.259348 sec +/- 0.078801
In [30]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 50.497971 s

Transformation mixup sur GPU¶

TODO : dans le script dlojz_da_2.py :

  • Appliquer la transformation mixup dans la boucle d'apprentissage aprĂšs avoir envoyĂ© le batch d'images et de labels au GPU, avec le paramĂ©trage : num_classes=1000, alpha=2, device=gpu.

TODO : dans le script mixup.py :

  • Ajouter le paramĂštre device=device Ă  chaque fois que l'on crĂ©e un nouveau Tensor pour qu'il soit stockĂ© en mĂ©moire au bon emplacement (CPU ou GPU).

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [31]:
command = f'dlojz_da_2.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 887909
jobid = ['887909']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [32]:
jobid = ['887909']
In [33]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            887909    gpu_p5     tutu  ssos938  R       2:11      1 jean-zay-iam16

 Done!
In [34]:
controle_technique(jobid)
Train throughput: 2188.81 images/second
GPU throughput: 2521.39 images/second
epoch time: 585.49 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.203062 sec (8.6%/110.8%) +/- 0.097770
loading step time average (IO + CPU to GPU transfer): 0.030855 sec +/- 0.063996
In [35]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 44.058701 s

Commentaires

TP_DA_3 : CutMix¶

Le but de ce TP est de :

  • appliquer la transformation CutMix et mesurer son impact sur la performance du code ;
  • adapter l'implĂ©mentation de la tranformation CutMix au calcul GPU.

La transformation CutMix n'est pas disponible dans torchvision, la fonction est disponible dans le script cutmix.py. On notera que cette transformation impacte Ă  la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le dataloader. Donc cette transformation sera faite dans la boucle d'apprentissage aprÚs génération du batch et donc aprÚs toute autre transformation liée à la Data Augmentation.

Dans le script cutmix.py, la variable _lambda correspond à la proportion conservée de la premiÚre image. Elle est choisie aléatoirement suivant une distribution uniforme définie sur [0, 1].

In [36]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[36]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /gpfsscratch/idris/sos/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear, antialias=warn)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [38]:
from cutmix import cutmix_data
In [41]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = cutmix_data(imgs, targets, num_classes=1000)

for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 224, 224]), data type: torch.float32, Memory usage: 9633792 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 492, lambda : 0.9307438135147095
target : 64, lambda : 0.901945173740387
target : 103, lambda : 0.8318518996238708
target : 776, lambda : 1.0
CPU times: user 6.52 s, sys: 242 ms, total: 6.76 s
Wall time: 6.96 s

Transformation CutMix sur GPU¶

TODO : dans le script dlojz_da_3.py :

  • Importer la transformation CutMix
from cutmix import cutmix_data
  • Rajouter la transformation CutMix dans la boucle d'apprentissage aprĂšs avoir envoyĂ© le batch d'images et de labels au GPU, avec le paramĂ©trage : num_classes=1000, device=gpu.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [42]:
command = f'dlojz_da_3.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00', qos='qos_gpu-dev')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 887951
jobid = ['887951']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [43]:
jobid = ['887951']
In [44]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            887951    gpu_p5     tutu  ssos938 CG       2:33      1 jean-zay-iam22

 Done!
In [45]:
controle_technique(jobid)
Train throughput: 1759.63 images/second
GPU throughput: 2512.19 images/second
epoch time: 728.30 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.203807 sec (7.0%/118.0%) +/- 0.104394
loading step time average (IO + CPU to GPU transfer): 0.087163 sec +/- 0.036166
In [46]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 48.079175 s

Optimisation de la transformation CutMix¶

Le code précédent traite les images du batch une par une, de maniÚre séquentielle (boucle for) :

mixed_x = x
for i in range(len(mixed_x)): # loop over images
            mixed_x[i,:,x1[i]:x2[i],y1[i]:y2[i]] = x[s_index[i],:,x1[i]:x2[i],y1[i]:y2[i]]

Le but de cette partie est d'optimiser le code de CutMix en générant davantage de parallélisme pour profiter du GPU. Il s'agit de supprimer la boucle for et de manipuler directement des batches de tenseurs.

Le travail va porter sur la définition de deux batches de masques mask_int et mask_ext de taille [batch_size,n_channels,weight,height] que l'on appliquera de la maniÚre suivante :

mixed_x = mask_ext * x + mask_int * x[s_index, :]

La fonction à implémenter est la suivante :

def cut_mask(x1, x2, y1, y2, batch_size, W, H, device=None):
    
    ### TODO
    mask_ext, mask_int = None, None
    
    return mask_ext, mask_int

Arguments :

  • x1 : vecteur de longueur batch_size avec la coordonnĂ©e min dans la largeur pour chaque image du batch
  • x2 : vecteur de longueur batch_size avec la coordonnĂ©e max dans la largeur pour chaque image du batch
  • y1 : vecteur de longueur batch_size avec la coordonnĂ©e min dans la hauteur pour chaque image du batch
  • y2 : vecteur de longueur batch_size avec la coordonnĂ©e max dans la hauteur pour chaque image du batch
  • batch_size : nombre d'images dans le batch
  • W : largeur des images
  • H : hauteur des images
  • device : unitĂ© de calcul ('cpu' ou 'gpu')

Retours:

  • mask_ext : tenseur de taille [batch_size,n_channels,weight,height] contenant la valeur False ou 0 Ă  l'intĂ©rieur de la fenĂȘtre et True ou 1 Ă  l'extĂ©rieur
  • mask_int : tenseur de taille [batch_size,n_channels,weight,height] contenant la valeur True ou 1 Ă  l'intĂ©rieur de la fenĂȘtre et False ou 0 Ă  l'extĂ©rieur

Création d'un batch de masques

Dans un premier temps, pour comprendre la procédure, nous travaillerons avec un batch de 3 images de taille 32x32.

In [92]:
import torch
import numpy as np
import matplotlib.pyplot as plt
batch_size = 3
W = 32
H = 32

En entrĂ©e, on connait les coordonnĂ©es des coins de la fenĂȘtre pour chaque image du batch (voir illustration ci-dessous).

In [93]:
# coordonnee min dans la largeur pour chaque image du batch
x1 = torch.Tensor([10, 5, 23]).int()
# coordonne max dans la largeur pour chaque image du batch
x2 =  torch.Tensor([20, 25, 31]).int()
# coordonnee min dans la hauteur pour chaque image du batch
y1 =  torch.Tensor([5, 10, 0]).int()
# coordonne max dans la hauteur pour chaque image du batch
y2 =  torch.Tensor([10, 22, 20]).int()

1. Création de w_int et h_int

Pour construire mask_int, on va d'abord créer un batch de vecteurs ligne "largeur" w_int et un batch de vecteurs colonne "hauteur" de masques h_int (voir illustration ci-dessus).

Variables utiles : batch_size, W, H, x1, x2, y1, y2.

Voir la documentation PyTorch pour la manipulation de tenseurs : documentation torch.

Résultats attendus : (voir illustration ci-dessous)

  • un batch de vecteurs ligne "largeur" w_int, tenseur de taille [3, 1, 32] contenant des True ou 1 si x1 ⩜ x ⩜ x2, False ou 0 sinon
  • un batch de vecteurs colonne "hauteur" h_int, tenseur de taille [3, 32, 1] contenant des True ou 1 si y1 ⩜ y ⩜ y2, False ou 0 sinon rĂ©sultat
Pistes de solutions

Nous avons trouvé 2 solutions pour résoudre ce problÚme.

  • En utilisant la fonction torch.logical_and : il s'agit d'initialiser des tenseurs Ă  [x, x=0,...,31] (respectivement [y, y=0,...,31]) et d'utiliser torch.logical_and() pour appliquer les conditions x1 ⩜ x and x ⩜ x2 (respectivement y1 ⩜ y and y ⩜ y2). Le rĂ©sultat est un tenseur contenant des opĂ©rateurs logiques True et False.

  • En utilisant la fonction torch.cumsum : les tenseurs sont initialisĂ©s Ă  0, on donne la valeur 1 au x1Ăšme Ă©lĂ©ment (respectivement y1Ăšme), la valeur -1 au x2Ăšme Ă©lĂ©ment (respectivement y2Ăšme), puis on utilise la fonction torch.cumsum pour faire la somme cumulĂ©e des Ă©lĂ©ments. On obtient un tenseur contenant des 0 et des 1.

Dans tous les cas, il faudra jouer avec les dimensions des tenseurs. Les fonctions utiles sont : torch.arange(), .size(), .repeat(), .view(), .unsqueeze(), .zeros(), ...

Il existe certainement d'autres solutions.

Rappel : ne jamais utiliser de for nulle part !

In [95]:
w_int = torch.zeros(batch_size,1,W) # vecteurs ligne 
w_int[range(batch_size),0,x1] = 1.
w_int[range(batch_size),0,x2] = -1.
w_int = torch.cumsum(w_int, dim=2).bool() # vecteurs ligne
# assert w_int has the correct size
assert w_int.size() == torch.Size([3,1,32])
In [96]:
for wx in w_int:
    plt.imshow(wx)
    plt.colorbar()
    plt.show()
In [97]:
h_int = torch.zeros(batch_size,H,1) # vecteurs colonne
h_int[range(batch_size),y1,0] = 1.
h_int[range(batch_size),y2,0] = -1.
h_int = torch.cumsum(h_int, dim=1).bool() # vecteurs colonne
# assert h_int has the correct size
assert h_int.size() == torch.Size([3,32,1])                              
In [98]:
for hx in h_int:
    plt.imshow(hx)
    plt.colorbar()
    plt.show()
Solution 1 avec torch.logical_and
# initialisation du tenseur w_int avec les valeurs [0,...,31]
w_int = torch.arange(W).repeat(batch_size,1,1) # batch de vecteurs ligne 
# Returns the mask applying ((x1 ⩜ x) and (x ⩜ x2))
w_int = torch.logical_and(w_int >= x1.view(-1,1,1), w_int <= x2.view(-1,1,1)) # vecteurs ligne

# initialisation du tenseur h_int avec les valeurs [0,...,31]
h_int = torch.arange(H).repeat(batch_size,1).unsqueeze(2) # batch de vecteurs colonne
# Returns the mask applying ((y1 ⩜ y) and (y ⩜ y2))
h_int = torch.logical_and(h_int >= y1.view(-1,1,1), h_int <= y2.view(-1,1,1)) # vecteurs colonne
Solution 2 avec torch.cumsum

On initialise les éléments correspondant aux coordonnées x1 et y1 à 1.
On initialise les éléments correspondant aux coordonnées x2 et y2 à -1.
Puis on utilise la fonction torch.cumsum pour remplir chaque intervalle [x1,x2] et [y1,y2] de 1, le reste de 0.

Remarque : il y a une petite erreur dans cette solution qui n'a pas d'impact majeur. +1 sur votre appréciation finale si vous la trouvez !!

w_int = torch.zeros(batch_size,1,W) # vecteurs ligne 
w_int[range(batch_size),0,x1] = 1.
w_int[range(batch_size),0,x2] = -1.
w_int = torch.cumsum(w_int, dim=2).bool() # vecteurs ligne

h_int = torch.zeros(batch_size,H,1) # vecteurs colonne
h_int[range(batch_size),y1,0] = 1.
h_int[range(batch_size),y2,0] = -1.
h_int = torch.cumsum(h_int, dim=1).bool() # vecteurs colonne

Solution +++

Avec la solution prĂ©cĂ©dente, les bornes x2 et y2 sont exclues de la fenĂȘtre. Pour les inclure, il faudrait dĂ©finir la valeur -1 sur les x2+1Ăšme et y2+1Ăšme Ă©lĂ©ments :

w_int[range(batch_size),0,x2+1] = -1.
h_int[range(batch_size),y2+1,0] = -1.

Cela entraßne des cas particuliers si x2=31 ou y2=31. Pour gérer ces exceptions sans introduire de if :

# if x2==31, set w_int(.,0,31)=0, otherwize set w_int(.,0,x2+1)=-1
w_int[.,0,torch.minimum(torch.tensor([31]).repeat(batch_size),x2+1)]=torch.maximum(torch.tensor([-1.]).repeat(batch_size),x2-31)
# if y2==31, set h_int(.,31,0)=0, otherwize set h_int(.,y2+1,0)=-1
h_int[.,torch.minimum(torch.tensor([31]).repeat(batch_size),y2+1,0)]=torch.maximum(torch.tensor([-1.]).repeat(batch_size),y2-31)

2. Création des batches de masques intérieurs et extérieurs

  • Multiplication des vecteurs h_int et w_int pour obtenir les masques intĂ©rieurs pour chaque image du batch.
In [99]:
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
In [100]:
# visualisation des masques intérieurs pour chaque image du batch
for m in mask_int:
    plt.imshow(m)
    plt.colorbar()
    plt.show()
Solution
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
  • Puis crĂ©ation des masques extĂ©rieurs Ă  partir des masques intĂ©rieurs.
Aide

Par exemple en utilisant la fonction torch.logical_not.

In [101]:
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = torch.logical_not(mask_int)
In [102]:
# visualisation des masques extérieurs
for m in mask_ext:
    plt.imshow(m)
    plt.colorbar()
    plt.show()
Solution
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = torch.logical_not(mask_int)

Implémentation de la fonction de création d'un batch de masques

Maintenant, l'idée est d'implémenter ce qui a été fait dans les cellules précédentes dans une fontion générique, en ajoutant un choix sur le device d'exécution.

TODO : implémenter la fonction de création des masques dans la cellule suivante. Les entrées de la fonction sont :

  • les coordonnĂ©es x1, x2, y1, y2,
  • le batch_size,
  • la largeurW de l'image,
  • la hauteur H de l'image,
  • le device de calcul.

Important : Pour les images RGB (channel de 3), il faut rajouter une dimension en deuxiĂšme position dans les masques finaux (doc .unsqueeze()) :

# rajouter une dimension en 2e position pour pouvoir traiter des images RGB
    mask_int = mask_int.unsqueeze(1) 
    mask_ext = mask_ext.unsqueeze(1)

Attention : Ne pas oublier le paramÚtre device=device à chaque création d'un nouveau Tensor. Par exemple pour :

w_int = torch.zeros(batch_size,1,W,device=device)
In [115]:
def cut_mask(x1, x2, y1, y2, batch_size, W, H, device=None):
    
    w_int = torch.zeros(batch_size,1,W, device=device) # vecteurs ligne 
    w_int[range(batch_size),0,x1] = 1.
    w_int[range(batch_size),0,x2] = -1.
    w_int = torch.cumsum(w_int, dim=2).bool() # vecteurs ligne
    
    h_int = torch.zeros(batch_size,H,1, device=device) # vecteurs colonne
    h_int[range(batch_size),y1,0] = 1.
    h_int[range(batch_size),y2,0] = -1.
    h_int = torch.cumsum(h_int, dim=1).bool() # vecteurs colonne
    
    # multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
    mask_int = h_int*w_int

    # les masques extérieurs sont les complémentaires des masques intérieurs
    mask_ext = torch.logical_not(mask_int)
    
    # rajouter une dimension en 2e position pour pouvoir traiter des images RGB
    mask_int = mask_int.unsqueeze(1) 
    mask_ext = mask_ext.unsqueeze(1)
    
    return mask_ext, mask_int

Test de la fonction implémentée¶

In [116]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
X train batch, shape: torch.Size([16, 3, 224, 224]), data type: torch.float32, Memory usage: 9633792 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
CPU times: user 4.37 s, sys: 34.6 ms, total: 4.4 s
Wall time: 4.56 s
In [117]:
batch_size = 16
W = image_size
H = image_size
In [118]:
lam = torch.rand(batch_size)
s_index = torch.randperm(batch_size)      # Shuffle index
rand_x = torch.randint(W, (batch_size,))
rand_y = torch.randint(H, (batch_size,))
cut_rat = torch.sqrt(1. - lam) ## cut ratio according to the random lambda

x1 = torch.clip(rand_x - rand_x / 2, min=0).long()
x2 = torch.clip(rand_x + rand_x / 2, max=W-1).long()
y1 = torch.clip(rand_y - rand_y / 2, min=0).long()
y2 = torch.clip(rand_y + rand_y / 2, max=H-1).long()

mask_ext, mask_int = cut_mask(x1, x2, y1, y2, batch_size, W, H)
In [119]:
# vĂ©rifier si le masque intĂ©rieur et l'image ont le mĂȘme nombre de dimensions
try:
    assert imgs.dim() == mask_int.dim()
    print('OK!')
except:
    print(f'Mismatch: \n dim imgs = {imgs.dim()} \n dim mask int = {mask_int.dim()} ')
OK!
In [120]:
# vĂ©rifier si le masque extĂ©rieur et l'image ont le mĂȘme nombre de dimensions
try:
    assert imgs.dim() == mask_ext.dim()
    print('OK!')
except:
    print(f'Mismatch: \n dim imgs = {imgs.dim()} \n dim mask ext = {mask_ext.dim()} ')
OK!
In [121]:
imgs = mask_ext * imgs + mask_int * imgs[s_index, :]
In [122]:
for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()

Puis si le résultat est satisfaisant, intégrer la fonction dans le code cutmix.py.

Intégration de la nouvelle version dans cutmix.py¶

TODO : dans le script cutmix.py, ajouter la fonction cut_mask définie dans la cellule plus haut.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [123]:
command = f'dlojz_da_3.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 888412
jobid = ['888412']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [124]:
jobid = ['888412']
In [126]:
display_slurm_queue(name)
 Done!
In [127]:
controle_technique(jobid)
Train throughput: 2294.66 images/second
GPU throughput: 2497.00 images/second
epoch time: 558.49 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.205046 sec (7.0%/121.1%) +/- 0.111344
loading step time average (IO + CPU to GPU transfer): 0.018081 sec +/- 0.001571
In [90]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 42.835168 s

Garage

In [ ]: